Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Raw β€’ Download

core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt df62b4fd384a7d3fb867a2879aa8a14f10cd8a29 (df62b4fd) Text, 35.41 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.database

Tff7b72import T7ee787androidx.datastore.core.DataStore
Tff7b72import T7ee787androidx.datastore.preferences.core.Preferences
Tff7b72import T7ee787androidx.datastore.preferences.core.booleanPreferencesKey
Tff7b72import T7ee787androidx.datastore.preferences.core.edit
Tff7b72import T7ee787androidx.datastore.preferences.core.intPreferencesKey
Tff7b72import T7ee787androidx.datastore.preferences.core.longPreferencesKey
Tff7b72import T7ee787androidx.datastore.preferences.core.stringPreferencesKey
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CompletableDeferred
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.NonCancellable
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.currentCoroutineContext
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.ensureActive
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.SharingStarted
Tff7b72import T7ee787kotlinx.coroutines.flow.StateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.filterNotNull
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.flow.map
Tff7b72import T7ee787kotlinx.coroutines.flow.stateIn
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.sync.Mutex
Tff7b72import T7ee787kotlinx.coroutines.sync.withLock
Tff7b72import T7ee787kotlinx.coroutines.withContext
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.koin.core.annotation.Named
Tff7b72import T7ee787org.koin.core.annotation.Single
Tff7b72import T7ee787org.meshtastic.core.common.util.normalizeAddress
Tff7b72import T7ee787org.meshtastic.core.common.util.nowMillis
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787kotlin.concurrent.Volatile
Tff7b72import T7ee787org.meshtastic.core.common.database.DatabaseManager Tff7b72as Te6edf3SharedDatabaseManager

T8b949e/** Manages per-device Room database instances for node data, with LRU eviction. */
Tf0883e@SingleTb4b4b4(Te6edf3binds Tff7b72= Tff7b72[Te6edf3DatabaseProviderTff7b72::Te6edf3classTb4b4b4, Te6edf3SharedDatabaseManagerTff7b72::Te6edf3classTff7b72]Tb4b4b4)
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4)
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72open Tff7b72class T56d364DatabaseManagerTb4b4b4(
Tf0883e@NamedTb4b4b4(Ta5d6ff"Ta5d6ffDatabaseDataStoreTa5d6ff"Tb4b4b4) Tff7b72private Tff7b72val Te6edf3datastoreTb4b4b4: Te6edf3DataStoreTff7b72<Te6edf3PreferencesTff7b72>Tb4b4b4,
Tff7b72private Tff7b72val Te6edf3dispatchersTb4b4b4: Te6edf3CoroutineDispatchersTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3DatabaseProviderTb4b4b4,
Te6edf3SharedDatabaseManager Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3managerScope Tff7b72= Te6edf3CoroutineScopeTb4b4b4(Te6edf3SupervisorJobTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3dispatchersTb4b4b4.Te6edf3defaultTb4b4b4)
Tff7b72private Tff7b72val Te6edf3mutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)

T8b949e// Per-source write barrier for merges. `withDb` deliberately does NOT take [mutex] (hot path), so a merge under
T8b949e// [mutex] must still drain any in-flight writer that captured the source DB before folding it away β€” otherwise a
T8b949e// late-committing write is lost when the source is retired. This dedicated lock (never held across a drain await,
T8b949e// so it can't deadlock the merge) tracks live `withDb` blocks per captured DB instance. It also guards the merge's
T8b949e// active-DB swap so a writer either registers against `source` before the swap and is drained, or captures `dest`
T8b949e// after it and is safe β€” it can never slip through the gap between the two.
Tff7b72private Tff7b72val Te6edf3writerTrackerMutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3activeWriters Tff7b72= Te6edf3mutableMapOfTff7b72<Te6edf3MeshtasticDatabaseTb4b4b4, Tffa657IntTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3drainWaiters Tff7b72= Te6edf3mutableMapOfTff7b72<Te6edf3MeshtasticDatabaseTb4b4b4, Te6edf3MutableListTff7b72<Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tff7b72>Tff7b72>Tb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3cacheLimitKey Tff7b72= Te6edf3intPreferencesKeyTb4b4b4(Te6edf3DatabaseConstantsTb4b4b4.Te6edf3CACHE_LIMIT_KEYTb4b4b4)
Tff7b72private Tff7b72val Te6edf3legacyCleanedKey Tff7b72= Te6edf3booleanPreferencesKeyTb4b4b4(Te6edf3DatabaseConstantsTb4b4b4.Te6edf3LEGACY_DB_CLEANED_KEYTb4b4b4)

Tff7b72private Tff7b72fun Td2a8fflastUsedKeyTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4) Tff7b72= Te6edf3longPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffdb_last_used:Tffd700$Te6edf3dbNameTa5d6ff"Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffaddrDbKeyTb4b4b4(Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4) Tff7b72=
Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Tffd700${Te6edf3DatabaseConstantsTb4b4b4.Te6edf3ADDR_DB_FOR_PREFIXTffd700}Tffd700${Te6edf3normalizeAddressTb4b4b4(Te6edf3addressTb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)

Tff7b72private Tff7b72var Te6edf3backfillJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null

Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3hasDelayedFirstDeviceBackfill Tff7b72= Tff7b72false

Tff7b72override Tff7b72val Te6edf3cacheLimitTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657IntTff7b72> Tff7b72=
Te6edf3datastoreTb4b4b4.Te6edf3data
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTff7b72[Te6edf3cacheLimitKeyTff7b72] Tff7b72?: Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_CACHE_LIMIT Tb4b4b4}
Tb4b4b4.Te6edf3stateInTb4b4b4(Te6edf3managerScopeTb4b4b4, Te6edf3SharingStartedTb4b4b4.Te6edf3EagerlyTb4b4b4, Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_CACHE_LIMITTb4b4b4)

Tff7b72override Tff7b72fun Td2a8ffgetCurrentCacheLimitTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Int Tff7b72= Te6edf3cacheLimitTb4b4b4.Te6edf3value

Tff7b72override Tff7b72fun Td2a8ffsetCacheLimitTb4b4b4(Te6edf3limitTb4b4b4: Tffa657IntTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3clamped Tff7b72= Te6edf3limitTb4b4b4.Te6edf3coerceInTb4b4b4(Te6edf3DatabaseConstantsTb4b4b4.Te6edf3MIN_CACHE_LIMITTb4b4b4, Te6edf3DatabaseConstantsTb4b4b4.Te6edf3MAX_CACHE_LIMITTb4b4b4)
Te6edf3managerScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTff7b72[Te6edf3cacheLimitKeyTff7b72] Tff7b72= Te6edf3clamped Tb4b4b4}
T8b949e// Enforce asynchronously with current active DB protected
Te6edf3enforceCacheLimitTb4b4b4(Te6edf3activeDbName Tff7b72= Te6edf3currentDbNameTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72val Te6edf3dbCache Tff7b72= Te6edf3mutableMapOfTff7b72<Tffa657StringTb4b4b4, Te6edf3MeshtasticDatabaseTff7b72>Tb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3_currentDb Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3MeshtasticDatabase?Tff7b72>Tb4b4b4(Tff7b72nullTb4b4b4)

T8b949e/**
* The currently active database, built lazily on first access. Room's `onOpen` callback is itself lazy (not invoked
* until the first query), so construction only allocates the builder and connection pool β€” actual I/O is deferred.
*/
Tff7b72override Tff7b72val Te6edf3currentDbTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3MeshtasticDatabaseTff7b72> Tff7b72=
Te6edf3_currentDb
Tb4b4b4.Te6edf3filterNotNullTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3stateInTb4b4b4(Te6edf3managerScopeTb4b4b4, Te6edf3SharingStartedTb4b4b4.Te6edf3EagerlyTb4b4b4, Te6edf3getOrOpenDatabaseTb4b4b4(Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_DB_NAMETb4b4b4)Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3_currentAddress Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3currentAddressTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657String?Tff7b72> Tff7b72= Te6edf3_currentAddress

T8b949e/**
* Name of the currently active database. Tracked explicitly rather than recomputed from the address, because
* cross-transport aliasing ([associateDevice]) decouples the two: a secondary transport's address maps to the DB
* claimed by the first transport, which `buildDbName(address)` would never produce. Written under [mutex].
*/
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3currentDbNameTb4b4b4: Tffa657String Tff7b72= Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_DB_NAME

T8b949e/** Initialize the active database for [address]. */
Tff7b72suspend Tff7b72fun Td2a8ffinitTb4b4b4(Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4) Tb4b4b4{
Te6edf3switchActiveDatabaseTb4b4b4(Te6edf3addressTb4b4b4)
Tb4b4b4}

T8b949e/**
* Returns a cached [MeshtasticDatabase] or builds a new one for [dbName]. The caller must hold [mutex] when
* modifying [dbCache] concurrently; however, this helper is also used from [currentDb]'s `initialValue` where the
* mutex is not yet relevant (single-threaded construction).
*/
Tff7b72private Tff7b72fun Td2a8ffgetOrOpenDatabaseTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3MeshtasticDatabase Tff7b72=
Te6edf3dbCacheTb4b4b4.Te6edf3getOrPutTb4b4b4(Te6edf3dbNameTb4b4b4) Tb4b4b4{ Te6edf3getDatabaseBuilderTb4b4b4(Te6edf3dbNameTb4b4b4)Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4) Tb4b4b4}

T8b949e/**
* Resolves the DB name to use for [address], honoring a cross-transport alias when one exists. A secondary
* transport (e.g. TCP) that has been unified with a node points at the DB the first transport (e.g. BLE) claimed;
* without an alias this falls back to the address-hashed name β€” today's default β€” for a first-time or primary
* connection. See [associateDevice].
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffresolveDbNameTb4b4b4(Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4)Tb4b4b4: Tffa657String Tb4b4b4{
Tff7b72val Te6edf3fallback Tff7b72= Te6edf3buildDbNameTb4b4b4(Te6edf3addressTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3fallback Tff7b72=Tff7b72= Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_DB_NAMETb4b4b4) Tff7b72return Te6edf3fallback
Tff7b72return Te6edf3datastoreTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tff7b72[Te6edf3addrDbKeyTb4b4b4(Te6edf3addressTb4b4b4)Tff7b72] Tff7b72?: Te6edf3fallback
Tb4b4b4}

T8b949e/** Switch active database to the one associated with [address]. Serialized via mutex. */
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffswitchActiveDatabaseTb4b4b4(Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4) Tff7b72= Te6edf3mutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72val Te6edf3dbName Tff7b72= Te6edf3resolveDbNameTb4b4b4(Te6edf3addressTb4b4b4)

T8b949e// Remember the previously active DB name (any) so we can record its last-used time as well.
Tff7b72val Te6edf3previousDbName Tff7b72= Tff7b72if Tb4b4b4(Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Te6edf3currentDbName Tff7b72else Tff7b72null

T8b949e// Fast path: no-op if already on this address
Tff7b72if Tb4b4b4(Te6edf3_currentAddressTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Te6edf3address Tff7b72&Tff7b72& Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3markLastUsedTb4b4b4(Te6edf3dbNameTb4b4b4)
Tff7b72returnTf0883e@withLock
Tb4b4b4}

T8b949e// Build/open Room DB off the main thread
Tff7b72val Te6edf3db Tff7b72= Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{ Te6edf3getOrOpenDatabaseTb4b4b4(Te6edf3dbNameTb4b4b4) Tb4b4b4}

T8b949e// Emit the new DB BEFORE closing the old one. flatMapLatest collectors on
T8b949e// currentDb will cancel their in-flight queries on the previous database once
T8b949e// the new value is emitted. Closing the old pool first would race with those
T8b949e// collectors, causing "Connection pool is closed" crashes.
Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72= Te6edf3db
Te6edf3_currentAddressTb4b4b4.Te6edf3value Tff7b72= Te6edf3address
Te6edf3currentDbName Tff7b72= Te6edf3dbName
Te6edf3markLastUsedTb4b4b4(Te6edf3dbNameTb4b4b4)
T8b949e// Also mark the previous DB as used "just now" so LRU has an accurate, recent timestamp
Te6edf3previousDbNameTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3markLastUsedTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}

T8b949e// Do NOT close the previous DB synchronously here. Even though _currentDb has been
T8b949e// updated, in-flight `withDb` calls may still hold a reference to the old database
T8b949e// (captured before the emission). Closing the connection pool while those queries are
T8b949e// executing causes "Connection pool is closed" crashes. Instead, let LRU eviction
T8b949e// (enforceCacheLimit) handle cleanup β€” it only runs on databases that are not the
T8b949e// active target and have not been used recently.

T8b949e// Defer LRU eviction so switch is not blocked by filesystem work
Te6edf3managerScopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{ Te6edf3enforceCacheLimitTb4b4b4(Te6edf3activeDbName Tff7b72= Te6edf3dbNameTb4b4b4) Tb4b4b4}

T8b949e// One-time cleanup: remove legacy DB if present and not active
Te6edf3managerScopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{ Te6edf3cleanupLegacyDbIfNeededTb4b4b4(Te6edf3activeDbName Tff7b72= Te6edf3dbNameTb4b4b4) Tb4b4b4}

T8b949e// Backfill FTS search index for any text messages missing messageText.
T8b949e// On the first real device DB, defer this so it does not starve the single DB connection while
T8b949e// the UI is collecting startup flows. The default DB should not consume the cold-start delay.
Tff7b72val Te6edf3shouldDelayBackfill Tff7b72= Te6edf3dbName Tff7b72!Tff7b72= Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_DB_NAME Tff7b72&Tff7b72& Tff7b72!Te6edf3hasDelayedFirstDeviceBackfill
Tff7b72if Tb4b4b4(Te6edf3shouldDelayBackfillTb4b4b4) Te6edf3hasDelayedFirstDeviceBackfill Tff7b72= Tff7b72true
Te6edf3scheduleSearchIndexBackfillTb4b4b4(Te6edf3dbName Tff7b72= Te6edf3dbNameTb4b4b4, Te6edf3db Tff7b72= Te6edf3dbTb4b4b4, Te6edf3shouldDelayBackfill Tff7b72= Te6edf3shouldDelayBackfillTb4b4b4)

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffSwitched active DB to Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3dbNameTb4b4b4)Tffd700}Ta5d6ff for address Tffd700${Te6edf3anonymizeAddressTb4b4b4(Te6edf3addressTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffassociateDeviceTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3deviceIdTb4b4b4: Tffa657String?Tb4b4b4) Tb4b4b4{
Te6edf3mutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72val Te6edf3sourceName Tff7b72= Te6edf3currentDbName
T8b949e// Never claim or merge into the sentinel "no device" DB.
Tff7b72if Tb4b4b4(Te6edf3sourceName Tff7b72=Tff7b72= Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_DB_NAMETb4b4b4) Tff7b72returnTf0883e@withLock

T8b949e// The device-id claim is the durable one (node numbers renumber under firmware 2.8); the
T8b949e// node-num claim stays as the fallback for hardware without a device id, for lockdown
T8b949e// sessions (device_id zeroed), and for claims written by older app versions. Writes always
T8b949e// refresh both keys so either lookup path resolves on the next connection.
Tff7b72val Te6edf3deviceKey Tff7b72= Te6edf3validDeviceIdOrNullTb4b4b4(Te6edf3deviceIdTb4b4b4)Tff7b72?.Te6edf3letTb4b4b4(Tff7b72::Te6edf3deviceDbPrefKeyTb4b4b4)
Tff7b72val Te6edf3nodeKey Tff7b72= Te6edf3nodeDbPrefKeyTb4b4b4(Te6edf3nodeNumTb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3datastoreTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3claimed Tff7b72= Te6edf3resolveDbClaimTb4b4b4(Te6edf3prefsTb4b4b4, Te6edf3deviceKeyTb4b4b4, Te6edf3nodeKeyTb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffwriteClaimsTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4) Tff7b72= Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{
Te6edf3deviceKeyTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3key Tff7b72-Tff7b72> Tffa657itTff7b72[Te6edf3keyTff7b72] Tff7b72= Te6edf3dbName Tb4b4b4}
Tffa657itTff7b72[Te6edf3nodeKeyTff7b72] Tff7b72= Te6edf3dbName
Tb4b4b4}

Tff7b72when Tb4b4b4{
Te6edf3claimed Tff7b72=Tff7b72= Tff7b72null Tff7b72-Tff7b72> Tb4b4b4{
T8b949e// First transport to learn this device: its current DB becomes the device's canonical DB.
T8b949e// No address alias is needed β€” a primary connection already resolves to this DB via buildDbName.
Te6edf3writeClaimsTb4b4b4(Te6edf3sourceNameTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffClaimed Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3sourceNameTb4b4b4)Tffd700}Ta5d6ff as canonical DB for node Tffd700$Te6edf3nodeNumTa5d6ff" Tb4b4b4}
Tb4b4b4}

Te6edf3claimed Tff7b72=Tff7b72= Te6edf3sourceName Tff7b72-Tff7b72> Tb4b4b4{
T8b949e// Already unified β€” just backfill/refresh any claim key that is missing or stale
T8b949e// (e.g. the first connect after this device renumbered, or after an app update
T8b949e// introduced device-id claims).
Tff7b72if Tb4b4b4(Tb4b4b4(Te6edf3deviceKey Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3prefsTff7b72[Te6edf3deviceKeyTff7b72] Tff7b72!Tff7b72= Te6edf3sourceNameTb4b4b4) Tff7b72|Tff7b72| Te6edf3prefsTff7b72[Te6edf3nodeKeyTff7b72] Tff7b72!Tff7b72= Te6edf3sourceNameTb4b4b4) Tb4b4b4{
Te6edf3writeClaimsTb4b4b4(Te6edf3sourceNameTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72else Tff7b72-Tff7b72> Tb4b4b4{
T8b949e// Secondary transport reached an already-known node: fold this DB into the canonical one,
T8b949e// switch the active DB to it, alias this address to it, and retire the now-merged source.
Tff7b72val Te6edf3source Tff7b72= Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72?: Tff7b72returnTf0883e@withLock
Tff7b72val Te6edf3dest Tff7b72= Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{ Te6edf3getOrOpenDatabaseTb4b4b4(Te6edf3claimedTb4b4b4) Tb4b4b4}

T8b949e// Redirect live writers to the canonical DB BEFORE merging (a connect triggers a full NodeDB
T8b949e// re-dump). The swap is published under the writer lock so a concurrent withDb write registers
T8b949e// against `source` before it β€” and is drained below β€” or captures `dest` after it and is safe.
T8b949e// New writers now capture `dest`; drainWriters then waits out any still writing to `source` so
T8b949e// the merge can't snapshot `source` mid-write and lose it when `source` is later retired.
Te6edf3publishActiveDbTb4b4b4(Te6edf3destTb4b4b4, Te6edf3claimedTb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
Te6edf3drainWritersTb4b4b4(Te6edf3sourceTb4b4b4, Te6edf3sourceNameTb4b4b4)
Te6edf3DatabaseMergerTb4b4b4.Te6edf3mergeTb4b4b4(Te6edf3sourceTb4b4b4, Te6edf3destTb4b4b4, Te6edf3sourceNameTb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
T8b949e// merge() is atomic, so on failure `dest` is unchanged. Roll the active DB back to
T8b949e// `source` so the address still resolves consistently and the merge retries next connect.
Te6edf3publishActiveDbTb4b4b4(Te6edf3sourceTb4b4b4, Te6edf3sourceNameTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ffMerge into Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3claimedTb4b4b4)Tffd700}Ta5d6ff failed; kept Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3sourceNameTb4b4b4)Tffd700}Ta5d6ff activeTa5d6ff"
Tb4b4b4}
Tff7b72returnTf0883e@withLock
Tb4b4b4}

Te6edf3markLastUsedTb4b4b4(Te6edf3claimedTb4b4b4)
T8b949e// Refresh both claim keys (migrates a legacy nodeNum-only claim forward to the
T8b949e// device-id key) and alias this transport's address to the canonical DB.
Te6edf3writeClaimsTb4b4b4(Te6edf3claimedTb4b4b4)
Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTff7b72[Te6edf3addrDbKeyTb4b4b4(Te6edf3_currentAddressTb4b4b4.Te6edf3valueTb4b4b4)Tff7b72] Tff7b72= Te6edf3claimed Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{
Ta5d6ff"Ta5d6ffUnified Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3sourceNameTb4b4b4)Tffd700}Ta5d6ff into Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3claimedTb4b4b4)Tffd700}Ta5d6ff for node Tffd700$Te6edf3nodeNumTa5d6ff"
Tb4b4b4}

T8b949e// Retire the merged source off the critical path; its data now lives in the canonical DB.
Te6edf3managerScopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{ Te6edf3retireDatabaseTb4b4b4(Te6edf3sourceNameTb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** Closes, deletes, and forgets a database whose contents have been merged into another. */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffretireDatabaseTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4) Tff7b72= Te6edf3mutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Te6edf3runCatching Tb4b4b4{
Te6edf3closeCachedDatabaseTb4b4b4(Te6edf3dbNameTb4b4b4)
Te6edf3deleteDatabaseTb4b4b4(Te6edf3dbNameTb4b4b4)
Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3lastUsedKeyTb4b4b4(Te6edf3dbNameTb4b4b4)Tb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3onSuccess Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffRetired merged DB Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3dbNameTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to retire merged database Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3dbNameTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Closes and removes a cached database by name. Safe to call even if the database was already closed or not in the
* cache. Does NOT delete the underlying file β€” the database can be re-opened on next access.
*
* On JVM/Desktop, Room KMP has no auto-close timeout (Android-only API), so idle databases hold open SQLite
* connections (5 per WAL-mode DB) indefinitely until explicitly closed. This method is the primary mechanism for
* releasing those connections when a database is no longer the active target.
*/
Tff7b72private Tff7b72fun Td2a8ffcloseCachedDatabaseTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3removed Tff7b72= Te6edf3dbCacheTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3dbNameTb4b4b4) Tff7b72?: Tff7b72return
Te6edf3runCatching Tb4b4b4{ Te6edf3removedTb4b4b4.Te6edf3closeTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to close cached database Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3dbNameTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4} Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffClosed inactive database Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3dbNameTb4b4b4)Tffd700}Ta5d6ff to free connectionsTa5d6ff" Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Reopens the active database under [mutex], but only if it hasn't switched since the caller snapshotted it.
*
* The replaced Room instance is intentionally left open for the rest of the process. [currentDb] is a derived
* StateFlow, so there is no deterministic handoff point where every app-wide collector has stopped using the old
* instance.
*
* Returns the reopened DB, or null if another coroutine already switched to a different device.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffreopenActiveDatabaseIfStillCurrentTb4b4b4(
Te6edf3expectedDbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4,
Te6edf3expectedDbNameTb4b4b4: Tffa657StringTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3MeshtasticDatabase? Tff7b72= Te6edf3mutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72!Tff7b72=Tff7b72= Te6edf3expectedDb Tff7b72|Tff7b72| Te6edf3currentDbName Tff7b72!Tff7b72= Te6edf3expectedDbNameTb4b4b4) Tff7b72return Tff7b72null

Tff7b72val Te6edf3cached Tff7b72= Te6edf3dbCacheTff7b72[Te6edf3expectedDbNameTff7b72]
Tff7b72if Tb4b4b4(Te6edf3cached Tff7b72!Tff7b72=Tff7b72= Te6edf3expectedDbTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffwithDb: active DB cache entry changed before reopen; skipping active DB reopenTa5d6ff" Tb4b4b4}
Tff7b72return Tff7b72null
Tb4b4b4}

T8b949e// Build a fresh instance directly (not through getOrPut) before touching the cache,
T8b949e// so a failed or cancelled build leaves the existing cache entry and _currentDb consistent.
Tff7b72val Te6edf3reopened Tff7b72= Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{ Te6edf3getDatabaseBuilderTb4b4b4(Te6edf3expectedDbNameTb4b4b4)Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3dbCacheTff7b72[Te6edf3expectedDbNameTff7b72] Tff7b72= Te6edf3reopened
Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72= Te6edf3reopened

T8b949e// Intentionally do not close expectedDb here. The public currentDb Flow is derived from
T8b949e// _currentDb through stateIn, so downstream flatMapLatest collectors may still be using the
T8b949e// replaced Room instance after this function emits the reopened DB. Closing the old pool here
T8b949e// can surface "Connection pool is closed" to app-wide DB observers that do not have closed-pool
T8b949e// recovery. This mirrors switchActiveDatabase's no-sync-close discipline; the leaked pool is
T8b949e// bounded by rare active-DB reopen recovery events and is reclaimed on process death. Revisit
T8b949e// once switching DB observers have explicit closed-pool resubscribe/retry handling.

Te6edf3reopened
Tb4b4b4}

T8b949e// Short-term runtime containment: route withDb entry through a single-lane dispatcher to narrow the Room/SQLite
T8b949e// connection-pool churn window seen during device/firmware update flows. Room suspend DAOs may continue on Room's
T8b949e// own executor after suspension, so this is not a strict global DB-I/O serialization guarantee. Preserve bounded
T8b949e// one-shot DB-critical blocks through cancellation, then re-check cancellation so stale callers do not continue
T8b949e// after the DB releases. Long-lived Flow/Paging reads must stay out of withDb; revisit after direct currentDb.value
T8b949e// callers are audited and safe DB concurrency can be restored.
Tff7b72private Tff7b72val Te6edf3limitedIo Tff7b72= Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4.Te6edf3limitedParallelismTb4b4b4(T79c0ff1Tb4b4b4)

T8b949e/** Execute [block] with the current DB instance. Retries once if the pool closes during a DB switch. */
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72suspend Tff7b72fun Tff7b72<Te6edf3TTff7b72> Td2a8ffwithDbTb4b4b4(Te6edf3blockTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3MeshtasticDatabaseTb4b4b4) Tff7b72-Tff7b72> Te6edf3TTb4b4b4)Tb4b4b4: Te6edf3T? Tb4b4b4{
Tff7b72val Te6edf3queuedAt Tff7b72= Te6edf3nowMillis
Tff7b72return Te6edf3withContextTb4b4b4(Te6edf3limitedIoTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3queuedMillis Tff7b72= Te6edf3nowMillis Tff7b72- Te6edf3queuedAt
Tff7b72if Tb4b4b4(Te6edf3queuedMillis Tff7b72>Tff7b72= Te6edf3WITH_DB_SLOW_OPERATION_MSTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffwithDb waited Tffd700${Te6edf3queuedMillisTffd700}Ta5d6ffms for the temporary DB containment laneTa5d6ff" Tb4b4b4}
Tb4b4b4}

Tff7b72val Te6edf3startedAt Tff7b72= Te6edf3nowMillis
Tff7b72try Tb4b4b4{
Te6edf3withCurrentDbTb4b4b4(Te6edf3blockTb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Tff7b72val Te6edf3elapsedMillis Tff7b72= Te6edf3nowMillis Tff7b72- Te6edf3startedAt
Tff7b72if Tb4b4b4(Te6edf3elapsedMillis Tff7b72>Tff7b72= Te6edf3WITH_DB_SLOW_OPERATION_MSTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Ta5d6ff"Ta5d6ffwithDb callback took Tffd700${Te6edf3elapsedMillisTffd700}Ta5d6ffms on the temporary DB containment lane; persistent Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffslow logs indicate DB access path should be revisitedTa5d6ff"
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Atomically snapshots the active DB and registers a writer against it. Before the first [switchActiveDatabase]
* `_currentDb` is still null, so fall back to the public [currentDb] view β€” the eagerly-opened default DB β€” giving
* withDb the exact DB-resolution semantics of a direct `currentDb.value` caller. Desktop never calls [init] until a
* device is selected, so without the fallback pre-connection writes (quick-chat actions, firmware/hardware cache
* refreshes) would silently no-op there instead of landing in the default DB the pre-connection flows read from.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffbeginWriteTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3MeshtasticDatabase Tff7b72= Te6edf3writerTrackerMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72val Te6edf3db Tff7b72= Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72?: Te6edf3currentDbTb4b4b4.Te6edf3value
Te6edf3activeWritersTff7b72[Te6edf3dbTff7b72] Tff7b72= Tb4b4b4(Te6edf3activeWritersTff7b72[Te6edf3dbTff7b72] Tff7b72?: T79c0ff0Tb4b4b4) Tff7b72+ T79c0ff1
Te6edf3db
Tb4b4b4}

T8b949e/**
* Registers a writer against a specific [db] β€” used by the withDb retry paths, whose target is a recovered/new
* instance rather than the snapshotted active DB, so their writes stay visible to a concurrent drain too.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffregisterWriterTb4b4b4(Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4) Tff7b72=
Te6edf3writerTrackerMutexTb4b4b4.Te6edf3withLock Tb4b4b4{ Te6edf3activeWritersTff7b72[Te6edf3dbTff7b72] Tff7b72= Tb4b4b4(Te6edf3activeWritersTff7b72[Te6edf3dbTff7b72] Tff7b72?: T79c0ff0Tb4b4b4) Tff7b72+ T79c0ff1 Tb4b4b4}

T8b949e/** Deregisters a writer and releases any merge waiting for [db] to quiesce. Cancellation-safe (see call site). */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffendWriteTb4b4b4(Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4) Tff7b72= Te6edf3writerTrackerMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72val Te6edf3remaining Tff7b72= Tb4b4b4(Te6edf3activeWritersTff7b72[Te6edf3dbTff7b72] Tff7b72?: T79c0ff1Tb4b4b4) Tff7b72- T79c0ff1
Tff7b72if Tb4b4b4(Te6edf3remaining Tff7b72<Tff7b72= T79c0ff0Tb4b4b4) Tb4b4b4{
Te6edf3activeWritersTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3dbTb4b4b4)
Te6edf3drainWaitersTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3dbTb4b4b4)Tff7b72?.Te6edf3forEach Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4) Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3activeWritersTff7b72[Te6edf3dbTff7b72] Tff7b72= Te6edf3remaining
Tb4b4b4}
Tb4b4b4}

T8b949e/** Publishes [db]/[name] as active under the writer lock so concurrent [beginWrite]s order against the swap. */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffpublishActiveDbTb4b4b4(Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4, Te6edf3nameTb4b4b4: Tffa657StringTb4b4b4) Tff7b72= Te6edf3writerTrackerMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72= Te6edf3db
Te6edf3currentDbName Tff7b72= Te6edf3name
Tb4b4b4}

T8b949e/**
* Suspends until every writer that captured [db] before this call has finished, so a merge never snapshots [db]
* while a write is still in flight (and then loses it when [db] is retired). Bounded by [WRITER_DRAIN_TIMEOUT_MS]
* so a wedged writer can't pin the merge β€” and [mutex] β€” forever; falling through on timeout is no worse than the
* old barrier-less behavior for that rare case.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffdrainWritersTb4b4b4(Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4, Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3waiter Tff7b72=
Te6edf3writerTrackerMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72if Tb4b4b4(Tb4b4b4(Te6edf3activeWritersTff7b72[Te6edf3dbTff7b72] Tff7b72?: T79c0ff0Tb4b4b4) Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Tff7b72return
Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3also Tb4b4b4{ Te6edf3drainWaitersTb4b4b4.Te6edf3getOrPutTb4b4b4(Te6edf3dbTb4b4b4) Tb4b4b4{ Te6edf3mutableListOfTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4.Te6edf3addTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3WRITER_DRAIN_TIMEOUT_MSTb4b4b4) Tb4b4b4{ Te6edf3waiterTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4) Tb4b4b4} Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffTimed out draining writers on Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3dbNameTb4b4b4)Tffd700}Ta5d6ff before mergeTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffThrowsCountTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffCyclomaticComplexMethodTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Tff7b72<Te6edf3TTff7b72> Td2a8ffwithCurrentDbTb4b4b4(Te6edf3blockTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3MeshtasticDatabaseTb4b4b4) Tff7b72-Tff7b72> Te6edf3TTb4b4b4)Tb4b4b4: Te6edf3T? Tb4b4b4{
Tff7b72val Te6edf3db Tff7b72= Te6edf3beginWriteTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3active Tff7b72= Te6edf3currentDbName
Te6edf3markLastUsedTb4b4b4(Te6edf3activeTb4b4b4)
Tff7b72try Tb4b4b4{
Tff7b72return Te6edf3runCancellableDbBlockTb4b4b4(Te6edf3dbTb4b4b4, Te6edf3blockTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e T8b949e// Preserve structured concurrency cancellation propagation.
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
T8b949e// If the active database switched while we held a reference to the old one,
T8b949e// and the exception indicates a closed pool/connection, retry with the new DB.
Tff7b72val Te6edf3retryDb Tff7b72= Te6edf3_currentDbTb4b4b4.Te6edf3value
Tff7b72if Tb4b4b4(Te6edf3retryDb Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3retryDb Tff7b72!Tff7b72=Tff7b72= Te6edf3db Tff7b72&Tff7b72& Te6edf3isDbClosedExceptionTb4b4b4(Te6edf3eTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffwithDb: database closed during switch (Tffd700${Te6edf3eTb4b4b4.Te6edf3messageTffd700}Ta5d6ff), retrying with current DBTa5d6ff" Tb4b4b4}
Tff7b72return Te6edf3retryRegisteredDbBlockTb4b4b4(Te6edf3retryDbTb4b4b4, Te6edf3eTb4b4b4, Te6edf3blockTb4b4b4)
Tb4b4b4}

T8b949e// Same active DB but Room's connection pool is wedged β€” reopen onto a fresh active instance once.
Tff7b72if Tb4b4b4(Te6edf3retryDb Tff7b72=Tff7b72=Tff7b72= Te6edf3db Tff7b72&Tff7b72& Te6edf3isDbPoolAcquireTimeoutExceptionTb4b4b4(Te6edf3eTb4b4b4)Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3reopened Tff7b72= Te6edf3reopenActiveDatabaseIfStillCurrentTb4b4b4(Te6edf3dbTb4b4b4, Te6edf3activeTb4b4b4)
Tff7b72val Te6edf3recoveredDb Tff7b72= Te6edf3reopened Tff7b72?: Te6edf3_currentDbTb4b4b4.Te6edf3valueTff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657it Tff7b72!Tff7b72=Tff7b72= Te6edf3db Tb4b4b4} Tff7b72?: Tff7b72throw Te6edf3e
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3reopened Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ffwithDb: reopened active DB after transient Room connection-pool timeoutTa5d6ff"
Tb4b4b4} Tff7b72else Tb4b4b4{
Ta5d6ff"Ta5d6ffwithDb: active DB switched during timeout recovery; retrying with current DBTa5d6ff"
Tb4b4b4}
Tb4b4b4}
Tff7b72return Te6edf3retryRegisteredDbBlockTb4b4b4(Te6edf3recoveredDbTb4b4b4, Te6edf3eTb4b4b4, Te6edf3blockTb4b4b4)
Tb4b4b4}

Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72finally Tb4b4b4{
T8b949e// NonCancellable so a cancelled withDb still deregisters β€” a leaked +1 would make every future
T8b949e// drain on this DB instance time out.
Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{ Te6edf3endWriteTb4b4b4(Te6edf3dbTb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Retries [block] against [db] β€” the recovered/new instance a withDb retry targets instead of the DB it originally
* registered against. Registers a writer on [db] for the duration so the retry write stays visible to a concurrent
* merge draining [db], with the same NonCancellable deregistration guarantee as [withCurrentDb]'s outer
* registration (which remains held on the original DB until that finally runs β€” the overlap is harmless, counts
* balance per instance). Any retry failure carries the original failure [cause] as a suppressed exception.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Tff7b72<Te6edf3TTff7b72> Td2a8ffretryRegisteredDbBlockTb4b4b4(
Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4,
Te6edf3causeTb4b4b4: Te6edf3ExceptionTb4b4b4,
Te6edf3blockTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3MeshtasticDatabaseTb4b4b4) Tff7b72-Tff7b72> Te6edf3TTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3T Tb4b4b4{
Te6edf3registerWriterTb4b4b4(Te6edf3dbTb4b4b4)
Tff7b72try Tb4b4b4{
Tff7b72return Te6edf3runCancellableDbBlockTb4b4b4(Te6edf3dbTb4b4b4, Te6edf3blockTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3retryCancelTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3retryCancel
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3retryExTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3retryExTb4b4b4.Te6edf3addSuppressedTb4b4b4(Te6edf3causeTb4b4b4)
Tff7b72throw Te6edf3retryEx
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{ Te6edf3endWriteTb4b4b4(Te6edf3dbTb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Tff7b72<Te6edf3TTff7b72> Td2a8ffrunCancellableDbBlockTb4b4b4(Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4, Te6edf3blockTb4b4b4: Te6edf3suspend Tb4b4b4(Te6edf3MeshtasticDatabaseTb4b4b4) Tff7b72-Tff7b72> Te6edf3TTb4b4b4)Tb4b4b4: Te6edf3T Tb4b4b4{
T8b949e// Keep withDb callbacks bounded and one-shot: NonCancellable can hold the containment lane until this returns.
Te6edf3currentCoroutineContextTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3ensureActiveTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{ Te6edf3blockTb4b4b4(Te6edf3dbTb4b4b4) Tb4b4b4}
Te6edf3currentCoroutineContextTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3ensureActiveTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3result
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffisDbClosedExceptionTb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Te6edf3isDbPoolAcquireTimeoutExceptionTb4b4b4(Te6edf3eTb4b4b4) Tff7b72|Tff7b72|
Te6edf3generateSequenceTff7b72<Te6edf3ThrowableTff7b72>Tb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3cause Tb4b4b4}
Tb4b4b4.Te6edf3any Tb4b4b4{ Te6edf3throwable Tff7b72-Tff7b72>
Tff7b72val Te6edf3msg Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3messageTff7b72?.Te6edf3lowercaseTb4b4b4(Tb4b4b4) Tff7b72?: Tff7b72returnTf0883e@any Tff7b72false
Tff7b72val Te6edf3hasDbContext Tff7b72= Te6edf3DB_TERMSTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657it Tff7b72in Te6edf3msg Tb4b4b4}
Tb4b4b4(Ta5d6ff"Ta5d6ffclosedTa5d6ff" Tff7b72in Te6edf3msg Tff7b72&Tff7b72& Te6edf3hasDbContextTb4b4b4) Tff7b72|Tff7b72| Ta5d6ff"Ta5d6ffdatabase is lockedTa5d6ff" Tff7b72in Te6edf3msg Tff7b72|Tff7b72| Ta5d6ff"Ta5d6ffsqlite_busyTa5d6ff" Tff7b72in Te6edf3msg
Tb4b4b4}

Tff7b72internal Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72private Tff7b72const Tff7b72val Te6edf3BACKFILL_COLD_START_DELAY_MS Tff7b72= T79c0ff2Te6edf3_000L
Tff7b72private Tff7b72const Tff7b72val Te6edf3WITH_DB_SLOW_OPERATION_MS Tff7b72= T79c0ff1Te6edf3_000L

T8b949e/**
* Upper bound on how long a merge waits for in-flight writers on the source DB to drain (see [drainWriters]).
*/
Tff7b72private Tff7b72const Tff7b72val Te6edf3WRITER_DRAIN_TIMEOUT_MS Tff7b72= T79c0ff5Te6edf3_000L
Tff7b72val Te6edf3DB_TERMS Tff7b72= Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffpoolTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffdatabaseTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffconnectionTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffsqliteTa5d6ff"Tb4b4b4)

Tff7b72private Tff7b72const Tff7b72val Te6edf3ROOM_POOL_ACQUIRE_TIMEOUT_PHRASE Tff7b72= Ta5d6ff"Ta5d6fftimed out attempting to acquireTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3ROOM_READER_CONNECTION_PHRASE Tff7b72= Ta5d6ff"Ta5d6ffreader connectionTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3ROOM_WRITER_CONNECTION_PHRASE Tff7b72= Ta5d6ff"Ta5d6ffwriter connectionTa5d6ff"

T8b949e/**
* Room KMP currently exposes pool-acquire timeouts as exception message text instead of a stable common typed
* signal. Keep this fallback narrow so BLE/GATT/transport connection errors do not trigger DB reopen recovery.
*/
Tff7b72private Tff7b72fun Td2a8ffisRoomPoolAcquireTimeoutMessageTb4b4b4(Te6edf3messageTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72=
Te6edf3ROOM_POOL_ACQUIRE_TIMEOUT_PHRASE Tff7b72in Te6edf3message Tff7b72&Tff7b72&
Tb4b4b4(Te6edf3ROOM_READER_CONNECTION_PHRASE Tff7b72in Te6edf3message Tff7b72|Tff7b72| Te6edf3ROOM_WRITER_CONNECTION_PHRASE Tff7b72in Te6edf3messageTb4b4b4)

Tff7b72fun Td2a8ffisDbPoolAcquireTimeoutExceptionTb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Te6edf3generateSequenceTff7b72<Te6edf3ThrowableTff7b72>Tb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3cause Tb4b4b4}
Tb4b4b4.Te6edf3any Tb4b4b4{ Te6edf3throwable Tff7b72-Tff7b72>
Tff7b72val Te6edf3msg Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3messageTff7b72?.Te6edf3lowercaseTb4b4b4(Tb4b4b4) Tff7b72?: Tff7b72returnTf0883e@any Tff7b72false
Te6edf3isRoomPoolAcquireTimeoutMessageTb4b4b4(Te6edf3msgTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Returns true if a database exists for the given device address. Android Room stores DB files without an
* extension; JVM/iOS append `.db`. We check both to stay platform-agnostic.
*/
Tff7b72override Tff7b72fun Td2a8ffhasDatabaseForTb4b4b4(Te6edf3addressTb4b4b4: Tffa657String?Tb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3addressTb4b4b4.Te6edf3isNullOrBlankTb4b4b4(Tb4b4b4) Tff7b72|Tff7b72| Te6edf3address Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4) Tff7b72return Tff7b72false
Tff7b72val Te6edf3dbName Tff7b72= Te6edf3buildDbNameTb4b4b4(Te6edf3addressTb4b4b4)
Tff7b72return Te6edf3dbFileExistsTb4b4b4(Te6edf3dbNameTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffdbFileExistsTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72val Te6edf3dir Tff7b72= Te6edf3getDatabaseDirectoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3fs Tff7b72= Te6edf3getFileSystemTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3fsTb4b4b4.Te6edf3existsTb4b4b4(Te6edf3dirTb4b4b4.Te6edf3resolveTb4b4b4(Te6edf3dbNameTb4b4b4)Tb4b4b4) Tff7b72|Tff7b72| Te6edf3fsTb4b4b4.Te6edf3existsTb4b4b4(Te6edf3dirTb4b4b4.Te6edf3resolveTb4b4b4(Ta5d6ff"Tffd700$Te6edf3dbNameTa5d6ff.dbTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffdbFileMetadataMillisTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657Long? Tb4b4b4{
Tff7b72val Te6edf3dir Tff7b72= Te6edf3getDatabaseDirectoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3fs Tff7b72= Te6edf3getFileSystemTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3fsTb4b4b4.Te6edf3metadataOrNullTb4b4b4(Te6edf3dirTb4b4b4.Te6edf3resolveTb4b4b4(Te6edf3dbNameTb4b4b4)Tb4b4b4)Tff7b72?.Te6edf3lastModifiedAtMillis
Tff7b72?: Te6edf3fsTb4b4b4.Te6edf3metadataOrNullTb4b4b4(Te6edf3dirTb4b4b4.Te6edf3resolveTb4b4b4(Ta5d6ff"Tffd700$Te6edf3dbNameTa5d6ff.dbTa5d6ff"Tb4b4b4)Tb4b4b4)Tff7b72?.Te6edf3lastModifiedAtMillis
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffmarkLastUsedTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3managerScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTff7b72[Te6edf3lastUsedKeyTb4b4b4(Te6edf3dbNameTb4b4b4)Tff7b72] Tff7b72= Te6edf3nowMillis Tb4b4b4} Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fflastUsedTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657Long Tb4b4b4{
Tff7b72val Te6edf3key Tff7b72= Te6edf3lastUsedKeyTb4b4b4(Te6edf3dbNameTb4b4b4)
Tff7b72val Te6edf3v Tff7b72= Te6edf3datastoreTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tff7b72[Te6edf3keyTff7b72] Tff7b72?: T79c0ff0L
Tff7b72return Tff7b72if Tb4b4b4(Te6edf3v Tff7b72=Tff7b72= T79c0ff0LTb4b4b4) Tb4b4b4{
Te6edf3dbFileMetadataMillisTb4b4b4(Te6edf3dbNameTb4b4b4) Tff7b72?: T79c0ff0L
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3v
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8fflistExistingDbNamesTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72> Tb4b4b4{
Tff7b72val Te6edf3dir Tff7b72= Te6edf3getDatabaseDirectoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3fs Tff7b72= Te6edf3getFileSystemTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3fsTb4b4b4.Te6edf3existsTb4b4b4(Te6edf3dirTb4b4b4)Tb4b4b4) Tff7b72return Te6edf3emptyListTb4b4b4(Tb4b4b4)

Tff7b72return Te6edf3fsTb4b4b4.Te6edf3listTb4b4b4(Te6edf3dirTb4b4b4)
Tb4b4b4.Te6edf3asSequenceTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3name Tb4b4b4}
Tb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3startsWithTb4b4b4(Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DB_PREFIXTb4b4b4) Tb4b4b4}
T8b949e// Skip Room-internal sidecar files (-wal/-shm/-journal) and lock files so each DB appears exactly once.
Tb4b4b4.Te6edf3filterNot Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3endsWithTb4b4b4(Ta5d6ff"Ta5d6ff-walTa5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3endsWithTb4b4b4(Ta5d6ff"Ta5d6ff-shmTa5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3endsWithTb4b4b4(Ta5d6ff"Ta5d6ff-journalTa5d6ff"Tb4b4b4) Tff7b72|Tff7b72| Tffa657itTb4b4b4.Te6edf3endsWithTb4b4b4(Ta5d6ff"Ta5d6ff.lckTa5d6ff"Tb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3removeSuffixTb4b4b4(Ta5d6ff"Ta5d6ff.dbTa5d6ff"Tb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3distinctTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3toListTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffenforceCacheLimitTb4b4b4(Te6edf3activeDbNameTb4b4b4: Tffa657StringTb4b4b4) Tff7b72= Te6edf3mutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72val Te6edf3limit Tff7b72= Te6edf3getCurrentCacheLimitTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3all Tff7b72= Te6edf3listExistingDbNamesTb4b4b4(Tb4b4b4)
T8b949e// Only enforce the limit over device-specific DBs; exclude legacy and default DBs
Tff7b72val Te6edf3deviceDbs Tff7b72=
Te6edf3allTb4b4b4.Te6edf3filterNot Tb4b4b4{ Tffa657it Tff7b72=Tff7b72= Te6edf3DatabaseConstantsTb4b4b4.Te6edf3LEGACY_DB_NAME Tff7b72|Tff7b72| Tffa657it Tff7b72=Tff7b72= Te6edf3DatabaseConstantsTb4b4b4.Te6edf3DEFAULT_DB_NAME Tb4b4b4}

Tff7b72if Tb4b4b4(Te6edf3deviceDbsTb4b4b4.Te6edf3size Tff7b72<Tff7b72= Te6edf3limitTb4b4b4) Tff7b72returnTf0883e@withLock
Tff7b72val Te6edf3usageSnapshot Tff7b72= Te6edf3deviceDbsTb4b4b4.Te6edf3associateWith Tb4b4b4{ Te6edf3lastUsedTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3victims Tff7b72= Te6edf3selectEvictionVictimsTb4b4b4(Te6edf3deviceDbsTb4b4b4, Te6edf3activeDbNameTb4b4b4, Te6edf3limitTb4b4b4, Te6edf3usageSnapshotTb4b4b4)

Te6edf3victimsTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3name Tff7b72-Tff7b72>
Te6edf3runCatching Tb4b4b4{
T8b949e// runCatching intentional: best-effort cleanup must not abort on cancellation
Te6edf3closeCachedDatabaseTb4b4b4(Te6edf3nameTb4b4b4)
Te6edf3deleteDatabaseTb4b4b4(Te6edf3nameTb4b4b4)
Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3removeTb4b4b4(Te6edf3lastUsedKeyTb4b4b4(Te6edf3nameTb4b4b4)Tb4b4b4) Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3onSuccess Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffEvicted cached DB Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3nameTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to evict database Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3nameTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffcleanupLegacyDbIfNeededTb4b4b4(Te6edf3activeDbNameTb4b4b4: Tffa657StringTb4b4b4) Tff7b72= Te6edf3mutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72val Te6edf3cleaned Tff7b72= Te6edf3datastoreTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tff7b72[Te6edf3legacyCleanedKeyTff7b72] Tff7b72?: Tff7b72false
Tff7b72if Tb4b4b4(Te6edf3cleanedTb4b4b4) Tff7b72returnTf0883e@withLock

Tff7b72val Te6edf3legacy Tff7b72= Te6edf3DatabaseConstantsTb4b4b4.Te6edf3LEGACY_DB_NAME
Tff7b72if Tb4b4b4(Te6edf3legacy Tff7b72=Tff7b72= Te6edf3activeDbNameTb4b4b4) Tb4b4b4{
Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTff7b72[Te6edf3legacyCleanedKeyTff7b72] Tff7b72= Tff7b72true Tb4b4b4}
Tff7b72returnTf0883e@withLock
Tb4b4b4}

Tff7b72if Tb4b4b4(Te6edf3dbFileExistsTb4b4b4(Te6edf3legacyTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3runCatching Tb4b4b4{
T8b949e// runCatching intentional: best-effort cleanup must not abort on cancellation
Te6edf3closeCachedDatabaseTb4b4b4(Te6edf3legacyTb4b4b4)
Te6edf3deleteDatabaseTb4b4b4(Te6edf3legacyTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3onSuccess Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDeleted legacy DB Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3legacyTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to delete legacy database Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3legacyTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}
Te6edf3datastoreTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTff7b72[Te6edf3legacyCleanedKeyTff7b72] Tff7b72= Tff7b72true Tb4b4b4}
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffscheduleSearchIndexBackfillTb4b4b4(Te6edf3dbNameTb4b4b4: Tffa657StringTb4b4b4, Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4, Te6edf3shouldDelayBackfillTb4b4b4: Tffa657BooleanTb4b4b4) Tb4b4b4{
Te6edf3backfillJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3backfillJob Tff7b72=
Te6edf3managerScopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3shouldDelayBackfillTb4b4b4) Te6edf3delayTb4b4b4(Te6edf3BACKFILL_COLD_START_DELAY_MSTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72!Tff7b72=Tff7b72= Te6edf3dbTb4b4b4) Tff7b72returnTf0883e@launch
Te6edf3backfillSearchIndexIfNeededTb4b4b4(Te6edf3dbTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to backfill search index for Tffd700${Te6edf3anonymizeDbNameTb4b4b4(Te6edf3dbNameTb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Backfills [Packet.messageText] for existing text-message packets that predate the FTS5 schema, then rebuilds the
* FTS index so search covers historical messages. The text is decoded in Kotlin from each packet's payload (see
* [PacketDao.backfillMessageTexts]); it cannot be read in SQL because the message body is stored as serialized
* `bytes`, not a `text` JSON field.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffbackfillSearchIndexIfNeededTb4b4b4(Te6edf3dbTb4b4b4: Te6edf3MeshtasticDatabaseTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3needsBackfill Tff7b72= Te6edf3dbTb4b4b4.Te6edf3packetDaoTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3countPacketsNeedingBackfillTb4b4b4(Tb4b4b4) Tff7b72> T79c0ff0
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3needsBackfillTb4b4b4) Tff7b72return

T8b949e// Perform the write operations inside NonCancellable to prevent
T8b949e// connection pool leaks due to coroutine cancellation.
Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3count Tff7b72= Te6edf3dbTb4b4b4.Te6edf3packetDaoTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3backfillMessageTextsTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3count Tff7b72> T79c0ff0Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffBackfilled Tffd700$Te6edf3countTa5d6ff messages for FTS search indexTa5d6ff" Tb4b4b4}
Te6edf3dbTb4b4b4.Te6edf3packetDaoTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3rebuildFtsIndexTb4b4b4(Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffFTS search index rebuild completeTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** Closes all open databases and cancels background work. */
Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3backfillJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3backfillJob Tff7b72= Tff7b72null
Te6edf3managerScopeTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3dbCacheTb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3forEach Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3closeTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3dbCacheTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)
Te6edf3_currentDbTb4b4b4.Te6edf3value Tff7b72= Tff7b72null
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.18s